/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.copier.annotations;

import java.beans.*;
import java.lang.reflect.*;
import ch.trackedbean.copier.*;
import ch.trackedbean.copier.engine.*;
import ch.trackedbean.loaders.lazy.*;

/**
 * Helper object for building an mapper based on annotations.
 * 
 * @author M. Hautle
 */
public class DefaultBeanMapperConfigurator extends BeanMapperConfigurator {
	/**
	 * Default constructor.
	 * 
	 * @param type The type for which the mapper gets built
	 */
	public DefaultBeanMapperConfigurator(Class<?> type) {
		beanMapper = new BeanMapper(getMappedBaseClass(type), type);
	}

	/**
	 * Creates and configures the mapper.
	 * 
	 * @return The created mapper
	 * @throws MapperInitialisationException if something went wrong
	 */
	public IBeanMapper create() throws MapperInitialisationException {
		try {
			inspectFields();
			inspectPropertyMethods();
			addInheritedMappings();
			cleanUp();
			return beanMapper;
		} catch (MapperInitialisationException e) {
			throw e;
		} catch (Exception e) {
			throw new MapperInitialisationException(e);
		}
	}

	/**
	 * Adds mappings inherited from other mappers.<br>
	 * At the moment only the mappings of the parent class were added.
	 */
	protected void addInheritedMappings() {
		final IBeanMapper mapper = BeanMapperManager.getMapper(beanMapper.getDstType().getSuperclass());
		if (mapper instanceof BeanMapper)
			beanMapper.addMappings((BeanMapper) mapper);
	}

	/**
	 * Extracts the value of {@link SourceClass#value()}.
	 * 
	 * @param type The to type
	 * @return The mapped base class or null
	 */
	private Class getMappedBaseClass(Class<?> type) {
		final SourceClass src = type.getAnnotation(SourceClass.class);
		return src != null ? src.value() : null;
	}

	/**
	 * Inspects the fields of the given class and registers the necessary mappings in the helper.
	 * 
	 * @throws InstantiationException If something wents wrong
	 * @throws IllegalAccessException If something wents wrong
	 */
	protected void inspectFields() throws InstantiationException, IllegalAccessException {
		// inspect the fields declared in this class
		for (Field f : beanMapper.getDstType().getDeclaredFields())
			processMappingOf(f.getName(), f);
	}

	/**
	 * Inspects the properties of the given class and registers the necessary mappings.
	 * 
	 * @throws IntrospectionException If something wents wrong
	 * @throws InstantiationException If something wents wrong
	 * @throws IllegalAccessException If something wents wrong
	 */
	protected void inspectPropertyMethods() throws IntrospectionException, InstantiationException, IllegalAccessException {
		final Class type = this.beanMapper.getDstType();
		final PropertyDescriptor[] props = Introspector.getBeanInfo(type).getPropertyDescriptors();
		if (props == null)
			return;
		for (PropertyDescriptor d : props) {
			final Method read = d.getReadMethod();
			// skip properties not declared in the current class
			if (type != read.getDeclaringClass())
				continue;
			final String name = d.getName();
			processMappingOf(name, read);
			addDefaultMapping(name);
		}
	}

	/**
	 * Processes the mapping annotations of the passed element.
	 * 
	 * @param name The property name
	 * @param el The annotated element
	 */
	protected void processMappingOf(String name, AnnotatedElement el) {
		if (ignoreMapping(name, el))
			return;
		addSimpleMapping(name, el);
		addExternalMapping(name, el);
		addPathMapping(name, el);
		addDeepMapping(name, el);
		addDeepPathMapping(name, el);
		addListMapping(name, el);
		addListPathMapping(name, el);
		addMultiMapping(name, el);
	}

	/**
	 * Adds the mappings for an {@link Mappings} annotation if there is one.
	 * 
	 * @param name The property name
	 * @param el The element
	 */
	protected void addMultiMapping(String name, AnnotatedElement el) {
		final Mappings mappings = el.getAnnotation(Mappings.class);
		if (mappings == null)
			return;
		for (DeepMapping m : mappings.deepMappings())
			addDeepMaping(name, m);
		for (DeepPathMapping m : mappings.deepPathMappings())
			addDeepPathMapping(name, m);
		for (ListMapping m : mappings.listMappings())
			addListMapping(name, m);
		for (ListPathMapping m : mappings.listPathMappings())
			addListPathMapping(name, m);
		for (PathMapping m : mappings.pathMappings())
			addPathMapping(name, m);
		for (SimpleMapping m : mappings.simpleMappings())
			addSimpleMapping(name, m);
	}

	/**
	 * Checks if the given element should not be mapped. Ensures that a call of {@link #addDefaultMapping(String)} does nothig if not desired.
	 * 
	 * @param name The property name
	 * @param el The property element
	 * @return True if the element should not inherit it's mapping
	 */
	protected boolean ignoreMapping(String name, AnnotatedElement el) {
		final boolean ignore = el.getAnnotation(IgnoreMapping.class) != null;
		if (ignore)
			ignoreMapping(name);
		return ignore;
	}

	/**
	 * Adds the mapping for an {@link SimpleMapping} if there is one.
	 * 
	 * @param property The property name
	 * @param el The element
	 */
	protected void addSimpleMapping(String property, AnnotatedElement el) {
		final SimpleMapping simple = el.getAnnotation(SimpleMapping.class);
		if (simple != null)
			addSimpleMapping(property, simple);
	}

	/**
	 * Adds the mapping for the given {@link SimpleMapping}.
	 * 
	 * @param property The property name
	 * @param simple The annotation
	 */
	protected void addSimpleMapping(String property, final SimpleMapping simple) {
		final String src = simple.value();
		final Class<?> srcType = getSourceType("SimpleMapping", property, simple.srcClass());
		final Class<? extends IPropertyConverter> converter = getClass(simple.converter(), IPropertyConverter.class);
		// self mapping?
		if (src.length() == 0)
			addSimpleMapping(srcType, property, converter);
		else
			addSimpleMapping(srcType, src, property, converter, simple.readOnly());
	}

	/**
	 * Adds the mapping for an {@link ExternalMapping} if there is one.
	 * 
	 * @param property The property name
	 * @param el The element
	 */
	protected void addExternalMapping(String property, AnnotatedElement el) {
		final ExternalMapping external = el.getAnnotation(ExternalMapping.class);
		if (external == null)
			return;
		for (Class<?> c : external.value())
			addSimpleMapping(c, property, property);
	}

	/**
	 * Adds the mapping for an {@link DeepMapping} if there is one.
	 * 
	 * @param property The property name
	 * @param el The element
	 */
	protected void addDeepMapping(String property, AnnotatedElement el) {
		final DeepMapping deep = el.getAnnotation(DeepMapping.class);
		if (deep != null)
			addDeepMaping(property, deep);
	}

	/**
	 * Adds the mapping for an {@link DeepMapping} annotation.
	 * 
	 * @param property The property name
	 * @param deep The annotation
	 */
	protected void addDeepMaping(String property, final DeepMapping deep) {
		final String src = deep.srcProperty();
		final Class<?> type = getSourceType("DeepMapping", property, deep.srcClass());
		addDeepMapping(type, src.length() > 0 ? src : property, property, deep.overrideSrc(), deep.overrideDst(), getClass(deep.srcEvaluator(),
				ITypeEvaluator.class), getClass(deep.dstEvaluator(), ITypeEvaluator.class), deep.readOnly(), isLazy(property));
	}

	/**
	 * Adds the mapping for an {@link DeepPathMapping} if there is one.
	 * 
	 * @param property The property name
	 * @param el The element
	 */
	protected void addDeepPathMapping(String property, AnnotatedElement el) {
		final DeepPathMapping deep = el.getAnnotation(DeepPathMapping.class);
		if (deep != null)
			addDeepPathMapping(property, deep);
	}

	/**
	 * Adds the mapping for an {@link DeepPathMapping} annotations.
	 * 
	 * @param property The property name
	 * @param deep The annotation
	 */
	protected void addDeepPathMapping(String property, final DeepPathMapping deep) {
		final Class<?> type = getSourceType("DeepPathMapping", property, deep.srcClass());
		addDeepPathMapping(type, deep.path(), deep.lastPathType(), property, deep.overrideSrc(), deep.overrideDst(), getClass(deep.srcEvaluator(),
				ITypeEvaluator.class), getClass(deep.dstEvaluator(), ITypeEvaluator.class), deep.readOnly(), isLazy(property));
	}

	/**
	 * Adds the mapping for an {@link ListMapping} if there is one.
	 * 
	 * @param property The property name
	 * @param el The element
	 */
	protected void addListMapping(String property, AnnotatedElement el) {
		final ListMapping list = el.getAnnotation(ListMapping.class);
		if (list != null)
			addListMapping(property, list);
	}

	/**
	 * Adds the mapping for an {@link ListMapping} annotation.
	 * 
	 * @param property The property name
	 * @param list The annotation
	 */
	protected void addListMapping(String property, final ListMapping list) {
		final String src = list.srcProperty();
		final Class<?> type = getSourceType("ListMapping", property, list.srcClass());
		final Class<? extends ITypeEvaluator> eval = getClass(list.srcEvaluator(), ITypeEvaluator.class);
		if (eval != null)
			addListMapping(type, src.length() > 0 ? src : property, property, eval, isLazy(property));
		else
			addStaticListMapping(type, src.length() > 0 ? src : property, property, list.dstType(), isLazy(property));
	}

	/**
	 * Adds the mapping for an {@link ListPathMapping} if there is one.
	 * 
	 * @param property The property name
	 * @param el The element
	 */
	protected void addListPathMapping(String property, AnnotatedElement el) {
		final ListPathMapping list = el.getAnnotation(ListPathMapping.class);
		if (list != null)
			addListPathMapping(property, list);
	}

	/**
	 * Adds the mapping for an {@link ListPathMapping} annotation.
	 * 
	 * @param property The property name
	 * @param list The annotation
	 */
	protected void addListPathMapping(String property, final ListPathMapping list) {
		final Class<?> type = getSourceType("ListPathMapping", property, list.srcClass());
		final Class<? extends ITypeEvaluator> eval = getClass(list.srcEvaluator(), ITypeEvaluator.class);
		if (eval != null)
			addListMapping(type, list.path(), list.lastPathType(), property, eval, isLazy(property));
		else
			addStaticListMapping(type, list.path(), list.lastPathType(), property, list.dstType(), isLazy(property));
	}

	/**
	 * Adds the mapping for an {@link PathMapping} if there is one.
	 * 
	 * @param property The property name
	 * @param el The element
	 */
	protected void addPathMapping(String property, AnnotatedElement el) {
		PathMapping path = el.getAnnotation(PathMapping.class);
		if (path != null)
			addPathMapping(property, path);
	}

	/**
	 * Adds the mapping for an {@link PathMapping} annotation.
	 * 
	 * @param property The property name
	 * @param path The annotation
	 */
	protected void addPathMapping(String property, PathMapping path) {
		final Class<?> type = getSourceType("PathMapping", property, path.srcClass());
		addPathMapping(type, property, type, path.path(), getClass(path.converter(), IPropertyConverter.class), path.readOnly());
	}

	/**
	 * Checks if the getter of the given property is annotated with {@link LoadLazy}.
	 * 
	 * @param property The property
	 * @return True if the property should be fetched lazy
	 */
	protected boolean isLazy(String property) {
		return isLazy(getDescriptor(beanMapper.getDstType(), property));
	}

	/**
	 * Checks if the getter of the given property is annotated with {@link LoadLazy}.
	 * 
	 * @param desc The property descriptor
	 * @return True if the property should be fetched lazy
	 */
	protected boolean isLazy(PropertyDescriptor desc) {
		final Method read = desc.getReadMethod();
		return read != null && read.getAnnotation(LoadLazy.class) != null;
	}

	/**
	 * Returns the source type.<br>
	 * Simply substitutes placeholder {@link Object}.class with {@link IBeanMapper#getMappedBaseType()}.
	 * 
	 * @param mapping The mapping type (only used for the exception)
	 * @param dstProp The destination property (only used for the exception)
	 * @param type The type
	 * @return The passed type or {@link IBeanMapper#getMappedBaseType()} if it was {@link Object}.class
	 * @throws MapperInitialisationException If there is no valid source type
	 */
	protected Class<?> getSourceType(String mapping, String dstProp, Class<?> type) {
		final Class res = type != Object.class ? type : beanMapper.getMappedBaseType();
		if (res == null)
			throw new MapperInitialisationException(mapping + " on " + type.getName() + "." + dstProp + " is only possible with defined "
					+ SourceClass.class.getName() + " annotation or defined source class!");
		return res;
	}

	/**
	 * Substitutes the passed base class throug <code>null</code>.
	 * 
	 * @param <T> The type
	 * @param value The class
	 * @param undefined The class value representing null
	 * @return The passed class or null
	 */
	protected <T> Class<? extends T> getClass(Class<? extends T> value, Class<T> undefined) {
		if (value == undefined)
			return null;
		return value;
	}
}